package io.xxx.xbutler.event.listener;

import cn.idev.excel.EasyExcel;
import cn.idev.excel.ExcelWriter;
import cn.idev.excel.support.ExcelTypeEnum;
import cn.idev.excel.write.metadata.WriteSheet;
import com.alibaba.fastjson2.JSONObject;
import com.aliyun.oss.OSS;
import io.xxx.xbutler.config.OssProperties;
import io.xxx.xbutler.constant.Formats;
import io.xxx.xbutler.constant.Topics;
import io.xxx.xbutler.data.ExportTaskMapper;
import io.xxx.xbutler.domain.ExportTask;
import io.xxx.xbutler.domain.ExportTaskStatus;
import io.xxx.xbutler.domain.ExportTaskType;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.pulsar.annotation.PulsarListener;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Slf4j
@Component
public class ExportTaskListener {

    private final Executor executor = Executors.newFixedThreadPool(5);

    @Resource
    private ExportTaskMapper exportTaskMapper;

    @Resource
    private SqlSession sqlSession;

    @Resource
    private SqlSessionFactory sqlSessionFactory;

    @Resource
    private OSS oss;

    @Resource
    private OssProperties ossProperties;

    @Value("${export.batchSize:5000}")
    private int batchSize;

    @Value("${export.zipSize:700000}")
    private int zipSize;

    @Value("${export.temp.dir:export}")
    private String tempDir;

    @Value("${export.oss.dir:leo/report/}")
    private String ossDir;

    @PulsarListener(topics = Topics.EXPORT_TASK)
    public void listen(Long id) {
        executor.execute(() -> export(id));
    }

    @SneakyThrows
    private void export(Long id) {
        ExportTask exportTask = exportTaskMapper.selectById(id);
        if (exportTask == null) {
            log.error("导出任务不存在[{}]", id);
            return;
        }

        try {
            // 1. 创建临时文件目录
            File dir = new File(tempDir, exportTask.getId().toString());
            create(dir);

            JSONObject params = exportTask.getParams();
            ExportTaskType type = exportTask.getType();
            Integer count = sqlSession.selectOne(type.getCountStatement(), params);
            // 2. 开始导出任务
            start(exportTask, count);

            Class<?> headClass = type.getHeadClass();

            List<File> files = new ArrayList<>();
            try (SqlSession session = sqlSessionFactory.openSession(false)) {
                File file = new File(getExcelFilename(exportTask, 1));
                Pair<ExcelWriter, WriteSheet> pair = buildExcel(file, headClass);
                ExcelWriter excelWriter = pair.getLeft();
                WriteSheet writeSheet = pair.getRight();

                List<Object> data = new ArrayList<>(batchSize);
                Cursor<Object> cursor = session.selectCursor(type.getDataStatement(), params);


                int index = 0;
                for (Object item : cursor) {
                    index++;

                    data.add(item);
                    if (data.size() == batchSize) {
                        excelWriter.write(data, writeSheet);
                        data.clear();

                        updateProcess(exportTask, index);
                    }
                    if (index % zipSize == 0) {
                        excelWriter.finish();

                        file = new File(getExcelFilename(exportTask, index / zipSize + 1));
                        files.add(file);
                        pair = buildExcel(file, headClass);
                        excelWriter = pair.getLeft();
                        writeSheet = pair.getRight();
                    }
                }

                if (!data.isEmpty()) {
                    excelWriter.write(data, writeSheet);
                    updateProcess(exportTask, index);
                }

                if (count == 0) {
                    excelWriter.write(Collections.emptyList(), writeSheet);
                }

                // 3. 上传文件
                upload(exportTask, files);
                cleanTempFiles(dir);
                end(exportTask);
            }
        } catch (Exception e) {
            interrupt(exportTask, e);
        }
    }

    private String getExcelFilename(ExportTask exportTask, int index) {
        return exportTask.getId().toString() + File.separator + exportTask.getName() + "_" + index + ".xlsx";
    }

    private Pair<ExcelWriter, WriteSheet> buildExcel(File file, Class<?> headClass) {
        ExcelWriter excelWriter = EasyExcel.write(file, headClass).excelType(ExcelTypeEnum.XLSX).build();
        WriteSheet writeSheet = EasyExcel.writerSheet("Sheet1").build();
        return Pair.of(excelWriter, writeSheet);
    }

    private void create(File dir) {
        if (dir.exists()) {
            deleteFiles(dir);
        } else {
            boolean success = dir.mkdirs();
            if (!success) {
                log.error("文件目录创建失败[{}]", dir.getAbsolutePath());
            }
        }
    }

    private void cleanTempFiles(File dir) {
        if (!dir.exists()) {
            return;
        }

        deleteFiles(dir);
        boolean success = dir.delete();
        if (!success) {
            log.error("文件目录删除失败[{}]", dir.getAbsolutePath());
        }
    }

    private void deleteFiles(File dir) {
        File[] files = dir.listFiles();
        if (!ObjectUtils.isEmpty(files)) {
            for (File file : files) {
                boolean success = file.delete();
                if (!success) {
                    log.error("文件删除失败[{}]", file.getAbsolutePath());
                }
            }
        }
    }

    private void upload(ExportTask exportTask, List<File> files) throws IOException {
        if (files.isEmpty()) {
            throw new IllegalArgumentException("文件列表为空");
        }
        exportTask.setStatus(ExportTaskStatus.UPLOADING)
                .setUpdatedTime(LocalDateTime.now());
        exportTaskMapper.updateById(exportTask);

        String filename = exportTask.getName() + exportTask.getId();
        String fileType;
        File file;
        if (files.size() == 1) {
            file = files.getFirst();
            fileType = ".xlsx";
        } else {
            File zipFile = new File(exportTask.getId() + File.separator + exportTask.getName() + ".zip");
            zip(zipFile, files, exportTask.getName());
            files.add(zipFile);
            file = zipFile;
            fileType = ".zip";
        }

        String key = ossDir + filename + fileType;
        oss.putObject(ossProperties.getBucket(), key, file);

        exportTask.setOssPath(key);
        exportTask.setUpdatedTime(LocalDateTime.now());
        exportTaskMapper.updateById(exportTask);
    }

    private void zip(File zipFile, List<File> files, String filename) throws IOException {
        try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(zipFile.toPath())); BufferedOutputStream out = new BufferedOutputStream(zip)) {
            for (int i = 0, filesSize = files.size(); i < filesSize; i++) {
                File file = files.get(i);
                try (FileInputStream fis = new FileInputStream(file)) {
                    zip.putNextEntry(new ZipEntry(filename + "_" + (i + 1) + ".xlsx"));

                    int len;
                    byte[] buffer = new byte[8192];
                    while ((len = fis.read(buffer)) > 0) {
                        out.write(buffer, 0, len);
                    }
                    out.flush();
                    zip.closeEntry();
                }
            }
        }
    }

    private void start(ExportTask exportTask, Integer count) {
        LocalDateTime now = LocalDateTime.now();
        exportTask.setCount(count)
                .setStatus(ExportTaskStatus.RUNNING)
                .setStartTime(now)
                .setUpdatedTime(now);
        exportTaskMapper.updateById(exportTask);
    }

    private void end(ExportTask exportTask) {
        LocalDateTime now = LocalDateTime.now();
        exportTask.setCleaned(true)
                .setStatus(ExportTaskStatus.COMPLETED)
                .setEndTime(now)
                .setUpdatedTime(now);
        exportTaskMapper.updateById(exportTask);
        if (log.isDebugEnabled()) {
            log.debug("导出{}[{}]任务完成。", exportTask.getName(), exportTask.getId());
        }
    }

    private void interrupt(ExportTask exportTask, Exception e) {
        exportTask.setStatus(ExportTaskStatus.INTERRUPTED)
                .setException(e.getLocalizedMessage())
                .setUpdatedTime(LocalDateTime.now());
        exportTaskMapper.updateById(exportTask);
    }

    private void updateProcess(ExportTask exportTask, int index) {
        double progress = 100.0 * index / exportTask.getCount();
        exportTask.setProgress(progress);
        exportTask.setUpdatedTime(LocalDateTime.now());
        exportTaskMapper.updateById(exportTask);
        if (log.isDebugEnabled()) {
            log.debug("导出{}[{}]任务进度 {}%。", exportTask.getName(), exportTask.getId(), Formats.NUMBER_FORMAT.format(progress));
        }
    }
}
